/**
 * Projet Multicast Bidois Morgan & Thomas Nunes
 *
 */
package Engine;

import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ClosedChannelException;
import java.nio.channels.ClosedSelectorException;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.nio.channels.spi.SelectorProvider;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import middleware.I_ClientMulticast;
import middleware.I_RecvMulticastHandler;
import middleware.I_ServerMulticast;

/**
 * Nio Engine
 */
public class NioEngine implements I_NioEngine {

    protected ServerSocketChannel serverSocketChannel;          //ServerSocket if it's a server
    protected SocketChannel channel2server = null;              //Socket if it's a client
    protected InetAddress address;
    protected Selector selector;
    protected Map< SocketChannel, InfosNio> mapBuffer;          //Collection of buffers: one per client connected
    protected ByteBuffer bufferTaille = ByteBuffer.allocate(4); //Buffer to stock the size of data received
    protected int port = 5555;
    protected InetAddress host = null;
    protected I_RecvMulticastHandler multicastHandler;          //Listener for callback
    private static final Logger logger = Logger.getLogger(NioEngine.class.getName());
    private boolean alive = true;

    //Differents states to DataIn method
    protected enum StateReceive {
        // Start to read the lenght, the name and the the data content.

        READ_LENGTH, READ_NAME, READ_CONTENT
    };
//    private I_ServerMulticastHandler serverHandlerMulticast;
//    private I_ClientMulticastHandler clientHandlerMulticast;

    @Override
    public void InitializeAsClient(InetAddress hostAddress, int port, I_RecvMulticastHandler handler) throws IOException {
        logger.log(Level.INFO, "Je suis le Client");
        this.address = host;
        this.selector = SelectorProvider.provider().openSelector();
        // non blocking server
        channel2server = SocketChannel.open();
        channel2server.configureBlocking(false);
        channel2server.connect(new InetSocketAddress(port));
        mapBuffer = new HashMap<>();
        //Add the server to map buffer to comunicate with him
        mapBuffer.put(this.channel2server, new InfosNio());
        // be notified when connection
        channel2server.register(selector, SelectionKey.OP_CONNECT);
        this.multicastHandler = handler;
        logger.setLevel(Level.WARNING);
    }

    public void InitializeAsClient(InetAddress hostAddress, int port, I_RecvMulticastHandler handler, I_ClientMulticast clientHandler) throws IOException {
        InitializeAsClient(hostAddress, port, handler);
//        this.clientHandlerMulticast = clientHandler;
    }

    @Override
    public void InitializeAsServer(InetAddress hostAddress, int port, I_RecvMulticastHandler handler) throws IOException {
        logger.log(Level.INFO, "Je suis le serveur");
        this.address = host;
        this.selector = SelectorProvider.provider().openSelector();
        // non blocking server
        serverSocketChannel = ServerSocketChannel.open();
        serverSocketChannel.configureBlocking(false);
        serverSocketChannel.socket().bind(new InetSocketAddress(address, port));

//        clientList = new HashMap<>();
        mapBuffer = new HashMap<>();

        this.multicastHandler = handler;
        // be notified when connection
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);// go to state ACCEPT
    }

    public void InitializeAsServer(InetAddress hostAddress, int port, I_RecvMulticastHandler handler, I_ServerMulticast serverHandler) throws IOException {
        this.InitializeAsServer(hostAddress, port, handler);
//        this.serverHandlerMulticast = serverHandler;
    }

    public void mainloop() {
        while (alive) {
            SelectionKey localKey = null;
            try {
                this.selector.select();
                Iterator selectedKeys = this.selector.selectedKeys().iterator();
                while (selectedKeys.hasNext()) {
                    localKey = (SelectionKey) selectedKeys.next();
                    selectedKeys.remove();
                    if (!localKey.isValid()) {
                        logger.log(Level.INFO, "Key invalide");
                        continue;
                    }
                    if (localKey.isConnectable()) {
                        handleConnect(localKey);
                    } else if (localKey.isAcceptable()) {
                        handleAccept(localKey);
                    } else if (localKey.isReadable()) {
                        handleDataIn(localKey);
                    } else if (localKey.isWritable()) {
                        handleDataOut(localKey);
                    }
                }
            } catch (ClosedSelectorException ce) {
                logger.log(Level.WARNING, "Connection close");
                this.terminate();
                alive = false;
                if (localKey != null && localKey.isValid()) {
                    SocketChannel localSocketChannel = (SocketChannel) localKey.channel();
                    this.multicastHandler.removeBySocket(localSocketChannel);
                } else {
                    logger.log(Level.SEVERE, "localkey invalid");
                }
            } catch (IOException ex) {
                logger.log(Level.SEVERE, "error in mainloop");
                this.terminate();// if we close server
            }
        }
    }

    @Override
    public void send(byte[] data, TypeMessage type, int idClient, int lc) {
        this.send(channel2server, data, type, idClient, lc);
    }

    @Override
    public void send(SocketChannel socketChannel, byte[] data, TypeMessage type, int idClient, int logicalClock) {
        SelectionKey localKey = null;
        Header frame = new Header(type, logicalClock, idClient);
        Message message = new Message(frame, data);
        if (mapBuffer.get(socketChannel) != null) {
            mapBuffer.get(socketChannel).addMessage2send(message);  //stock the message in buffer
            localKey = socketChannel.keyFor(this.selector);
            logger.log(Level.INFO, "****Passage mode write****");
        }
        if (localKey != null) {
            // write mode but we still read new incomming mesages.
            localKey.interestOps(SelectionKey.OP_WRITE & -SelectionKey.OP_READ);
        } else {
            //So the client is dead, we notify the multicast layer to delete him from directory
            this.multicastHandler.notifyAClientIsAFK(idClient, socketChannel);
            logger.log(Level.WARNING, "A client disapeer !!");
        }
        //We wake up the selector cause this fonction is call by GUI layer
        this.selector.wakeup();
    }

    @Override
    public void terminate() {
        try {
            if (channel2server != null) {
                channel2server.close();
            }
            if (selector != null) {
                selector.close();
            }
            //Close all sockets
            for (SocketChannel soc : this.mapBuffer.keySet()) {
                soc.socket().close();
                soc.close();
            }
        } catch (IOException ex) {
            logger.log(Level.SEVERE, "imposible a terminate!", ex);
        }
    }

    /**
     * Connect to the server
     *
     * @param key the key coresponding to the socketchannel
     */
    private void handleConnect(SelectionKey key) {
        SocketChannel localSocketChannel = (SocketChannel) key.channel();
        try {
            if (localSocketChannel.finishConnect()) {
                logger.log(Level.INFO, "Connection...");
                logger.log(Level.INFO, "****Passage mode read lenght****");
                key.interestOps(SelectionKey.OP_READ);
            } else {
                logger.log(Level.WARNING, "wait to connect...");
            }
        } catch (Exception ex) {
            logger.log(Level.WARNING, "except handle connect");
        }
    }

    /**
     * Accept a connection from a client
     *
     * @param key the key coresponding to the socketchannel
     */
    private void handleAccept(SelectionKey key) {
        try {
            ServerSocketChannel serverSocket = (ServerSocketChannel) key.channel();
            //accept the connection
            logger.log(Level.INFO, "wait client");
            SocketChannel localSocketChannel = serverSocket.accept();
            localSocketChannel.configureBlocking(false);
            localSocketChannel.finishConnect();

            //Register
            logger.log(Level.INFO, "Add a client");
            this.multicastHandler.putSocket(localSocketChannel);
            mapBuffer.put(localSocketChannel, new InfosNio());
            logger.log(Level.INFO, "****go to read name mode****");
            localSocketChannel.register(selector, SelectionKey.OP_READ);
        } catch (IOException ex) {
            logger.log(Level.WARNING, "except handle accept", ex);
        }
    }

    /**
     * receive data from a socket
     *
     * @param key the key coresponding to the socketchannel
     */
    private void handleDataIn(SelectionKey key) {
        SocketChannel localSocketChannel = (SocketChannel) key.channel();

        //Get infos related to the key:
        ByteBuffer bufferIn = mapBuffer.get(localSocketChannel).getBufferIn();
        StateReceive buffState = mapBuffer.get(localSocketChannel).getBuffState();

        bufferIn.clear();
        bufferTaille.clear();
        int numRead = 0;
        try {
            if (buffState == NioEngine.StateReceive.READ_LENGTH) {
                //Listen size and header
                numRead = localSocketChannel.read(bufferTaille);
                if (bufferTaille.remaining() == 0) {
                    bufferTaille.rewind();
                    int tailleMessage = bufferTaille.getInt();
                    buffState = NioEngine.StateReceive.READ_CONTENT;
                    bufferIn = ByteBuffer.allocate(tailleMessage);

                    //Get the frame now (a short int)
                    ByteBuffer bufferFrame = ByteBuffer.allocate(2 + 4 + 4);
                    bufferFrame.clear();
                    localSocketChannel.read(bufferFrame);
                    bufferFrame.rewind();
                    short shFlag = bufferFrame.getShort();
                    int clock = bufferFrame.getInt();
                    int idClientReceived = bufferFrame.getInt();
                    Header head = new Header(shFlag, clock, idClientReceived);
                    mapBuffer.get(localSocketChannel).setHeader(head);
                }
            }
            //read the content message
            if (buffState == NioEngine.StateReceive.READ_CONTENT) {
                numRead = localSocketChannel.read(bufferIn);
                if (bufferIn.remaining() == 0) {
                    String msg = new String(bufferIn.array());

                    Header header = mapBuffer.get(localSocketChannel).getHeader();
                    Message message = new Message(header, bufferIn.array());
                    this.receiveMessage(message, localSocketChannel);
                    buffState = NioEngine.StateReceive.READ_LENGTH;
                }
                bufferIn.flip();
            }
            if (numRead == -1) { //If socket is close
                try {
                    key.channel().close();
                    key.cancel();
                } catch (IOException ex) {
                    logger.log(Level.WARNING, "a client is disconnect");
                }
            }
        } catch (IOException ex) {
            logger.log(Level.WARNING, "except handle dataIn");
            try {
                key.channel().close();
            } catch (IOException ex1) {
                logger.log(Level.WARNING, "except handle dataIn imposible to close", ex1);
            }
            key.cancel();
        }
    }

    /**
     * Send data to a socket channel
     *
     * @param key the key coresponding to the socketchannel
     */
    private void handleDataOut(SelectionKey key) {
        SocketChannel outSocketChannel = (SocketChannel) key.channel();
        try {
            Message message = mapBuffer.get(outSocketChannel).getMessage2send();
            if (message != null) {
                ByteBuffer bufferOut = message.toByte();
                bufferOut.clear();

                if (bufferOut.hasRemaining()) {
                    outSocketChannel.write(bufferOut);

                    logger.log(Level.INFO, "Sending message: " + new String(bufferOut.array()));
                    //start to read
                }
                if (!bufferOut.hasRemaining()) {
                    // if no more message to send, go to read state.
                    if (mapBuffer.get(outSocketChannel).isEmptyWaitingList()) {
                        key.interestOps(SelectionKey.OP_READ);
                        logger.log(Level.INFO, "****go to read mode****");
                    }
                }
            }
        } catch (IOException ex) {
            logger.log(Level.WARNING, "except handle dataOut");
            key.cancel();
            try {
                outSocketChannel.close();
            } catch (IOException ex1) {
                logger.log(Level.WARNING, "except handle dataout impossible to close");
            }
        }
    }

    //@TODO voir pour les buffer si faut en mettre aussi ...
    public void createSocketServer(InetAddress address, int port) {
        try {
            this.serverSocketChannel = ServerSocketChannel.open();
            this.serverSocketChannel.configureBlocking(false);
            this.serverSocketChannel.socket().bind(new InetSocketAddress(address, port));
        } catch (IOException ex) {
            logger.log(Level.SEVERE, null, "imposible to create");
        }
        try {
            this.serverSocketChannel.register(this.selector, SelectionKey.OP_ACCEPT);
        } catch (ClosedChannelException ex) {
            logger.log(Level.SEVERE, "fail create socket server", ex);
        }
    }

    public SocketChannel connectClientToAnotherClient(InetAddress adrClientToCo, int portClientToCo, int idClientToCo) {
        SocketChannel socket = null;
        try {
            socket = SocketChannel.open();
            socket.configureBlocking(false);
            socket.connect(new InetSocketAddress(adrClientToCo, portClientToCo));
            this.mapBuffer.put(socket, new InfosNio());
            this.multicastHandler.putSocket(socket);
            socket.register(this.selector, SelectionKey.OP_CONNECT);
            return socket;
        } catch (IOException ex) {
            Logger.getLogger(NioEngine.class.getName()).log(Level.SEVERE, "fail connect to an another client", ex);
        }
        return socket;
    }

    /**
     * Action to do when the nio engine received a message
     *
     * @param message the message received
     */
    public void receiveMessage(Message message, SocketChannel socket) {
        System.out.println("type receive: " + message.getHeader().getType());
        multicastHandler.receivedCB(message, socket);
    }
}
